//
// (C) Copyright 2009 Irantha Suwandarathna (irantha@gmail.com)
// All rights reserved.
//

/* Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using EffiProz.Core.Lib;
using EffiProz.Core.Errors;
using EffiProz.Core.Results;
using EffiProz.Core.Navigators;

namespace EffiProz.Core.ScriptIO
{
    //
    // todo - can lock the database engine as _readonly _in a wrapper for this when
    // used at checkpoint

    /**
     * Handles all logging to file operations. A log consists of three blocks:<p>
     *
     * DDL BLOCK: definition of DB objects, users and rights at startup time<br>
     * DATA BLOCK: all data for MEMORY tables at startup time<br>
     * LOG BLOCK: SQL statements logged since startup or the last CHECKPOINT<br>
     *
     * The implementation of this class and its subclasses support the formats
     * used for writing the data. In versions up to 1.7.2, this data is written
     * to the *.script file for the database. Since 1.7.2 the data can also be
     * written as binray _in order to speed up shutdown and startup.<p>
     *
     * In 1.7.2, two separate files are used, one for the DDL + DATA BLOCK and
     * the other for the LOG BLOCK.<p>
     *
     * A related use for this class is for saving a current snapshot of the
     * database data to a user-defined file. This happens _in the SHUTDOWN COMPACT
     * process or done as a result of the SCRIPT command. In this case, the
     * DATA block contains the CACHED table data as well.<p>
     *
     * DatabaseScriptReader and its subclasses read back the data at startup time.
     *
     * @author fredt@users
     * @version 1.8.0
     * @since 1.7.2
     */
    public abstract class ScriptWriterBase
    {
        Database database;
        protected String outFile;
        protected Stream fileStreamOut;
        IFileSync outDescriptor;
        //int tableRowCount;
        public QNameManager.QName schemaToLog;
        bool isClosed;

        /**
         * this determines if the script is the normal script (false) used
         * internally by the engine or a user-initiated snapshot of the DB (true)
         */
        bool isDump;
        bool includeCachedData;
        protected long byteCount;
        protected volatile bool needsSync;
        protected volatile bool forceSync;
        public volatile bool busyWriting;
        private int syncCount;
        const int INSERT = 0;
        const int INSERT_WITH_SCHEMA = 1;

        /** the last schema for last sessionId */
        public Session currentSession;
        public static String[] LIST_SCRIPT_FORMATS = new String[] {
        Tokens.T_TEXT, Tokens.T_BINARY, null, Tokens.T_COMPRESSED
    };

        public ScriptWriterBase(Database db, String file, bool includeCachedData,
                         bool isNewFile, bool isDump)
        {

            this.isDump = isDump;

            initBuffers();

            bool exists = false;

            if (isDump)
            {
                exists = FileUtil.GetDefaultInstance().exists(file);
            }
            else
            {
                exists = db.logger.getFileAccess().IsStreamElement(file);
            }

            if (exists && isNewFile)
            {
                throw Error.error(ErrorCode.FILE_IO_ERROR, file);
            }

            this.database = db;
            this.includeCachedData = includeCachedData;
            outFile = file;
            currentSession = database.sessionManager.getSysSession();

            // start with neutral schema - no SET SCHEMA to log
            schemaToLog = currentSession.loggedSchema =
                currentSession.currentSchema;

            openFile();
        }

        public void reopen()
        {
            openFile();
        }

        protected abstract void initBuffers();

        /**
         *  Called internally or externally in write delay intervals.
         */
        public void sync()
        {

            if (isClosed)
            {
                return;
            }

            lock (fileStreamOut)
            {
                if (needsSync)
                {
                    if (busyWriting)
                    {
                        forceSync = true;

                        return;
                    }

                    try
                    {
                        fileStreamOut.Flush();
                        outDescriptor.Sync();

                        syncCount++;
                    }
                    catch (IOException e)
                    {
                        Error.printSystemOut("flush() or sync() error: "
                                             + e.ToString());
                    }

                    needsSync = false;
                    forceSync = false;
                }
            }
        }

        public void close()
        {

            stop();

            if (isClosed)
            {
                return;
            }

            try
            {
                lock (fileStreamOut)
                {
                    needsSync = false;
                    isClosed = true;

                    fileStreamOut.Flush();
                    outDescriptor.Sync();
                    fileStreamOut.Close();
                }
            }
            catch (IOException )
            {
                throw Error.error(ErrorCode.FILE_IO_ERROR);
            }

            byteCount = 0;
        }

        public long size()
        {
            return byteCount;
        }

        public void writeAll()
        {

            try
            {
                writeDDL();
                writeExistingData();
                finishStream();
            }
            catch (IOException )
            {
                throw Error.error(ErrorCode.FILE_IO_ERROR);
            }
        }

        /**
         *  File is opened in append mode although in current usage the file
         *  never pre-exists
         */
        protected void openFile()
        {

            try
            {
                EfzFileAccess fa = isDump ? FileUtil.GetDefaultInstance()
                                          : database.logger.getFileAccess();
                Stream fos = fa.OpenOutputStreamElement(outFile);

                outDescriptor = fa.GetFileSync(fos);
#if !SILVERLIGHT
                fileStreamOut = new BufferedStream(fos, 2 << 12);
#else
                fileStreamOut = fos;
#endif
            }
            catch (IOException e)
            {
                throw Error.error(e, ErrorCode.FILE_IO_ERROR,
                                  ErrorCode.M_Message_Pair, new Object[] {
                e.Message, outFile
            });
            }
        }

        /**
         * This is not really useful in the current usage but may be if this
         * class is used in a different way.
         */
        protected virtual void finishStream() { }

        protected void writeDDL()
        {

            Result ddlPart = database.getScript(!includeCachedData);

            writeSingleColumnResult(ddlPart);
        }

        protected void writeExistingData()
        {

            // start with blank schema - SET SCHEMA to log
            currentSession.loggedSchema = null;

            Iterator schemas = database.schemaManager.allSchemaNameIterator();

            while (schemas.hasNext())
            {
                String schema = (String)schemas.next();
                Iterator tables =
                    database.schemaManager.databaseObjectIterator(schema,
                        SchemaObjectTypes.TABLE);

                while (tables.hasNext())
                {
                    Table t = (Table)tables.next();

                    // write all memory table data
                    // write cached table data unless index roots have been written
                    // write all text table data apart from readonly text tables
                    // unless index roots have been written
                    bool script = false;

                    switch (t.getTableType())
                    {

                        case TableBase.MEMORY_TABLE:
                            script = true;
                            break;

                        case TableBase.CACHED_TABLE:
                            script = includeCachedData;
                            break;

                        case TableBase.TEXT_TABLE:
                            script = includeCachedData && !t.isReadOnly();
                            break;
                    }

                    try
                    {
                        if (script)
                        {
                            schemaToLog = t.getName().schema;

                            writeTableInit(t);

                            RowIterator it = t.rowIterator(currentSession);

                            while (it.hasNext())
                            {
                                writeRow(currentSession, t,
                                         it.getNextRow().getData());
                            }

                            writeTableTerm(t);
                        }
                    }
                    catch (Exception e)
                    {
                        throw Error.error(ErrorCode.FILE_IO_ERROR, e.ToString());
                    }
                }
            }

            writeDataTerm();
        }

        protected virtual void writeTableInit(Table t) { }

        protected void writeTableTerm(Table t)
        {

            if (t.isDataReadOnly() && !t.isTemp() && !t.isText())
            {
                StringBuilder a = new StringBuilder("SET TABLE ");

                a.Append(t.getName().statementName);
                a.Append(" READONLY TRUE");
                writeLogStatement(currentSession, a.ToString());
            }
        }

        protected void writeSingleColumnResult(Result r)
        {

            RowSetNavigator nav = r.initialiseNavigator();

            while (nav.hasNext())
            {
                Object[] data = (Object[])nav.getNext();

                writeLogStatement(currentSession, (String)data[0]);
            }
        }

        public abstract void writeRow(Session session, Table table,
                               Object[] data);

        protected abstract void writeDataTerm();

        protected abstract void addSessionId(Session session);

        public abstract void writeLogStatement(Session session,
                                               String s);

        public abstract void writeInsertStatement(Session session, Table table,
                Object[] data);

        public abstract void writeDeleteStatement(Session session, Table table,
                Object[] data);

        public abstract void writeSequenceStatement(Session session,
                NumberSequence seq);

        public abstract void writeCommitStatement(Session session);

        //
        private Object timerTask;

        // long write delay for scripts : 60s
        protected volatile int writeDelay = 60000;

        public void run()
        {

            try
            {
                if (writeDelay != 0)
                {
                    sync();
                }

                /** @todo: try to do Cache.cleanUp() here, too */
            }
            catch (Exception )
            {

                // ignore exceptions
                // may be InterruptedException or IOException
            }
        }

        public void setWriteDelay(int delay)
        {

            writeDelay = delay;

            int period = writeDelay == 0 ? 1000
                                         : writeDelay;

            EfzTimer.setPeriod(timerTask, period);
        }

        public void start()
        {

            int period = writeDelay == 0 ? 1000
                                         : writeDelay;

            timerTask = DatabaseManager.getTimer().schedulePeriodicallyAfter(0,
                    period, new ThreadStart(run), false);
        }

        public void stop()
        {

            if (timerTask != null)
            {
                EfzTimer.cancel(timerTask);

                timerTask = null;
            }
        }

        public int getWriteDelay()
        {
            return writeDelay;
        }
    }
}